iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Mobile Development

在 iOS 專案上加上 Unit testing - 因為 You need testing系列 第 19

D19 - 在 iOS 專案加上測試-You need testing {台股小工具 app-測 UserDefaults part1}

  • 分享至 

  • xImage
  •  

在 UIKit 時代,將資料存在裝置上,最常使用的物件就是 UserDefaults,但 UserDefaults 並沒有辦法和 SwiftUI 的 @State 等 property wrapper 搭配良好。雖然這個 Demo App 是用 SwiftUI 寫的,但我們仍然可以用 UIViewControllerRepresentable 來進行 UIKit 與 SwiftUI 元件的溝通。

先寫一個每按一下,就 tap count 數就 + 1 的 VC。auto layout 使用 SnapKit,這樣就可以用純程式碼來寫文章。

import SnapKit
import UIKit

class TapCounterViewController: UIViewController {
    
    private var count: Int = 0 {
        didSet {
            countLabel.text = "已經點擊了 \(count) 下"
            UserDefaults.standard.set(count, forKey: "tapCount")
        }
    }
    
    private lazy var countLabel: UILabel = .init()
    
    private lazy var addButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("按下去就 + 1", for: .normal)
        button.addTarget(self, action: #selector(countButtonDidTap), for: .touchUpInside)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        count = UserDefaults.standard.integer(forKey: "tapCount")
    }
    
    private func setupUI() {
        
        countLabel.textAlignment = .center
        view.addSubview(countLabel)
        countLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.equalTo(200)
            make.height.equalTo(44)
        }
        
        view.addSubview(addButton)
        addButton.snp.makeConstraints { make in
            make.width.height.centerX.equalTo(countLabel)
            make.top.equalTo(countLabel.snp.bottom).offset(10)
        }
    }
    
    @objc
    private func countButtonDidTap() {
        count += 1
        print("current tap count: \(count)")
    }
}

然後,再開一個 SwiftUI 的 View,把這個 VC 裝進去

import SwiftUI
import UIKit

/// 裝載 UIKit VC 的物件
struct TapCounterRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> TapCounterViewController {
        TapCounterViewController()
    }
    
    func updateUIViewController(_ uiViewController: TapCounterViewController, context: Context) {
    }
    
    typealias UIViewControllerType = TapCounterViewController
    
}

struct TapCounterView: View {
    var body: some View {
        /// 一個包著 SwiftUI 皮的 UIKit VC
        TapCounterRepresentable()
    }
}

struct TapCounterView_Previews: PreviewProvider {
    static var previews: some View {
        TapCounterView()
    }
}

因為是 SwiftUI View 包住的 VC,當然可以用 Preview 來看狀況。這個 feature 在今年的 Xcode 15 應該已經是標配了,不過筆者現在還在 Xcode 14.2 的版本,所以沒有 #preview 的方法來看 UIKit

https://ithelp.ithome.com.tw/upload/images/20230930/20140622IOyacsfUlq.png

實際將模擬器 run 起來,並關掉後再打開,你可以看到 tap count 有確實的紀錄起來。

UserDefaults 這種 persistant storage 還是 singleton 可以測試嗎?

可以的,我們就來測試 UserDefaults

測試 UserDefaults 的方法

step1: 開一個新檔案,並命名為 UserDefaultsProtocol,在裡面新增一個 UserDefaultsProtocol

/// UserDefaultsProtocol.swift
import Foundation

protocol UserDefaultsProtocol {}

step2: 在 VC 宣告 userDefaults 時,指定型別為 UserDefaultsProtocol 但實作是 UserDefaults.standard,然後把 VC 裡面的 UserDefaults.standard 換成 userDefaults

/// TapCounterViewController.swift
var userDefaults: UserDefaultsProtocol = UserDefaults.standard

然後,你會看到 Xcode 跳出 error,因為 UserDefaultsProtocol 的型別和 UserDefaults 不合

https://ithelp.ithome.com.tw/upload/images/20230930/20140622Sak9cOkVp0.png

step3: 將 UserDefaults extension UserDefaultsProtocol

extension UserDefaults: UserDefaultsProtocol {}

然後,你會看到其他錯誤,因為 UserDefaultsProtocol 在這個專案沒有兩個被 VC 使用的 func/property

https://ithelp.ithome.com.tw/upload/images/20230930/20140622PwMpR0rdIf.png

step4: 擴充 UserDefaultsProtocol

從 Xcode 的文件裡面,可以找到 UserDefaults 裡面 func 的宣告,雖然 Objective-C 有點難讀,但如果真的要找,也是可以在 Apple 的官方文件中找到 integer 的宣告。並把這兩個 func 補上。

https://developer.apple.com/documentation/foundation/userdefaults/1413614-set

https://developer.apple.com/documentation/foundation/userdefaults/1407405-integer

/// UserDefaultsProtocol.swift
protocol UserDefaultsProtocol {
    func set(_ value: Int, forKey defaultName: String)
    func integer(forKey defaultName: String) -> Int
}

step5: 開始測試 UserDefaultsProtocol ,並在 Test target 加上 Fake Object 物件

//  UserDefaultsTests.swift
import XCTest
@testable import TwStockTools

class FakeUserDefaults: UserDefaultsProtocol {
    
    var integers: [String: Int] = [:]
    
    func set(_ value: Int, forKey defaultName: String) {
        integers[defaultName] = value
    }
    
    func integer(forKey defaultName: String) -> Int {
        integers[defaultName] ?? 0
    }
}

接下來,下一篇,我們要用這個 FakeUserDefaults 進行在 VC 中進行測試


上一篇
D18 - 在 iOS 專案加上測試-You need testing {台股小工具 app-StockRecord InputView 和 RecordStore 的組裝}
下一篇
D20 - 在 iOS 專案加上測試-You need testing {台股小工具 app-測 UserDefaults part2}
系列文
在 iOS 專案上加上 Unit testing - 因為 You need testing32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言